/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springsource.restbucks.payment.web;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONObject;
import java.nio.file.Files;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.ResultActions;
import org.springsource.restbucks.AbstractWebIntegrationTest;
import org.springsource.restbucks.Restbucks;
import org.springsource.restbucks.order.Order;
import com.jayway.jsonpath.JsonPath;
/**
* Integration tests modeling the hypermedia-driven interaction flow against the server implementation. Uses the Spring
* MVC integration test facilities introduced in 3.2. Implements the order process modeled in my presentation on
* Hypermedia design with Spring.
*
* @see http://bit.ly/UIcDvq
* @author Oliver Gierke
*/
@Slf4j
public class PaymentProcessIntegrationTest extends AbstractWebIntegrationTest {
private static final String ORDERS_REL = Restbucks.CURIE_NAMESPACE + ":orders";
private static final String ORDER_REL = Restbucks.CURIE_NAMESPACE + ":order";
private static final String RECEIPT_REL = Restbucks.CURIE_NAMESPACE + ":receipt";
private static final String CANCEL_REL = Restbucks.CURIE_NAMESPACE + ":cancel";
private static final String UPDATE_REL = Restbucks.CURIE_NAMESPACE + ":update";
private static final String PAYMENT_REL = Restbucks.CURIE_NAMESPACE + ":payment";
private static final String FIRST_ORDER_EXPRESSION = String.format("$._embedded.%s[0]", ORDERS_REL);
/**
* Processes the first existing {@link Order} found.
*/
@Test
public void processExistingOrder() throws Exception {
MockHttpServletResponse response = accessRootResource();
response = discoverOrdersResource(response);
response = accessFirstOrder(response);
response = triggerPayment(response);
response = pollUntilOrderHasReceiptLink(response);
response = takeReceipt(response);
verifyOrderTaken(response);
}
/**
* Creates a new {@link Order} and processes that one.
*/
@Test
public void processNewOrder() throws Exception {
MockHttpServletResponse response = accessRootResource();
response = createNewOrder(response);
response = triggerPayment(response);
response = pollUntilOrderHasReceiptLink(response);
response = takeReceipt(response);
verifyOrderTaken(response);
}
/**
* Creates a new {@link Order} and cancels it right away.
*/
@Test
public void cancelOrderBeforePayment() throws Exception {
MockHttpServletResponse response = accessRootResource();
response = createNewOrder(response);
cancelOrder(response);
}
/**
* Access the root resource by referencing the well-known URI. Verifies the orders resource being present.
*
* @return the response for the orders resource
* @throws Exception
*/
private MockHttpServletResponse accessRootResource() throws Exception {
LOG.info("Accessing root resource…");
MockHttpServletResponse response = mvc.perform(get("/")). //
andExpect(status().isOk()). //
andExpect(linkWithRelIsPresent(ORDERS_REL)). //
andReturn().getResponse();
return response;
}
/**
* Creates a new {@link Order} by looking up the orders link from the source and posting the content of
* {@code orders.json} to it. Verifies we receive a {@code 201 Created} and a {@code Location} header. Follows the
* location header to retrieve and verify the {@link Order} just created.
*
* @param source
* @return
* @throws Exception
*/
private MockHttpServletResponse createNewOrder(MockHttpServletResponse source) throws Exception {
String content = source.getContentAsString();
Link ordersLink = getDiscovererFor(source).findLinkWithRel(ORDERS_REL, content);
ClassPathResource resource = new ClassPathResource("order.json");
byte[] data = Files.readAllBytes(resource.getFile().toPath());
MockHttpServletResponse result = mvc
.perform(post(ordersLink.expand().getHref()).contentType(MediaType.APPLICATION_JSON).content(data)). //
andExpect(status().isCreated()). //
andExpect(header().string("Location", is(notNullValue()))). //
andReturn().getResponse();
return mvc.perform(get(result.getHeader("Location"))).andReturn().getResponse();
}
/**
* Follows the {@code orders} link returns the {@link Order}s found.
*
* @param source
* @return
* @throws Exception
*/
private MockHttpServletResponse discoverOrdersResource(MockHttpServletResponse source) throws Exception {
String content = source.getContentAsString();
Link ordersLink = getDiscovererFor(source).findLinkWithRel(ORDERS_REL, content);
LOG.info("Root resource returned: " + content);
LOG.info(String.format("Found orders link pointing to %s… Following…", ordersLink));
MockHttpServletResponse response = mvc.perform(get(ordersLink.expand().getHref())). //
andExpect(status().isOk()). //
andReturn().getResponse();
LOG.info("Found orders: " + response.getContentAsString());
return response;
}
/**
* Looks up the first {@link Order} from the orders representation using a JSONPath expression of
* {@value #FIRST_ORDER_EXPRESSION}. Looks up the {@value Link#REL_SELF} link from the nested object and follows it to
* lookup the representation. Verifies the {@code self}, {@code cancel}, and {@code update} link to be present.
*
* @param source
* @return
* @throws Exception
*/
private MockHttpServletResponse accessFirstOrder(MockHttpServletResponse source) throws Exception {
String content = source.getContentAsString();
String order = JsonPath.parse(content).read(JsonPath.compile(FIRST_ORDER_EXPRESSION), JSONObject.class).toString();
Link orderLink = getDiscovererFor(source).findLinkWithRel("self", order).expand();
LOG.info(String.format("Picking first order using JSONPath expression %s…", FIRST_ORDER_EXPRESSION));
LOG.info(String.format("Discovered self link pointing to %s… Following", orderLink));
return mvc.perform(get(orderLink.getHref())). //
andExpect(linkWithRelIsPresent(Link.REL_SELF)). //
andExpect(linkWithRelIsPresent(CANCEL_REL)). //
andExpect(linkWithRelIsPresent(UPDATE_REL)). //
andExpect(linkWithRelIsPresent(PAYMENT_REL)).//
andReturn().getResponse();
}
/**
* Triggers the payment of an {@link Order} by following the {@code payment} link and submitting a credit card number
* to it. Verifies that we get a {@code 201 Created} and the response contains an {@code order} link. After the
* payment has been triggered we fake a cancellation to make sure it is rejected with a {@code 404 Not found}.
*
* @param response
* @return
* @throws Exception
*/
private MockHttpServletResponse triggerPayment(MockHttpServletResponse response) throws Exception {
String content = response.getContentAsString();
LinkDiscoverer discoverer = getDiscovererFor(response);
Link paymentLink = discoverer.findLinkWithRel(PAYMENT_REL, content);
LOG.info(String.format("Discovered payment link pointing to %s…", paymentLink));
assertThat(paymentLink, not(nullValue()));
LOG.info("Triggering payment…");
ResultActions action = mvc.perform(put(paymentLink.getHref()).//
content("\"1234123412341234\"").//
contentType(MediaType.APPLICATION_JSON).//
accept(MediaTypes.HAL_JSON));
MockHttpServletResponse result = action.andExpect(status().isCreated()). //
andExpect(linkWithRelIsPresent(ORDER_REL)). //
andReturn().getResponse();
LOG.info("Payment triggered…");
// Make sure we cannot cheat and cancel the order after it has been payed
LOG.info("Faking a cancel request to make sure it's forbidden…");
Link selfLink = discoverer.findLinkWithRel(Link.REL_SELF, content).expand();
mvc.perform(delete(selfLink.getHref())).andExpect(status().isMethodNotAllowed());
return result;
}
/**
* Polls the order resource every 2 seconds and uses an {@code If-None-Match} header alongside the {@code ETag} of the
* first response to avoid sending the representation over and over again.
*
* @param response
* @return
* @throws Exception
*/
private MockHttpServletResponse pollUntilOrderHasReceiptLink(MockHttpServletResponse response) throws Exception {
// Grab
String content = response.getContentAsString();
LinkDiscoverer discoverer = getDiscovererFor(response);
Link orderLink = discoverer.findLinkWithRel(ORDER_REL, content);
// Poll order until receipt link is set
Link receiptLink = null;
String etag = null;
MockHttpServletResponse pollResponse;
do {
HttpHeaders headers = new HttpHeaders();
if (etag != null) {
headers.setIfNoneMatch(etag);
}
LOG.info("Poll state of order until receipt is ready…");
ResultActions action = mvc.perform(get(orderLink.expand().getHref()).headers(headers));
pollResponse = action.andReturn().getResponse();
int status = pollResponse.getStatus();
etag = pollResponse.getHeader("ETag");
LOG.info(String.format("Received %s with ETag of %s…", status, etag));
if (status == HttpStatus.OK.value()) {
action.andExpect(linkWithRelIsPresent(Link.REL_SELF)). //
andExpect(linkWithRelIsNotPresent(UPDATE_REL)). //
andExpect(linkWithRelIsNotPresent(CANCEL_REL));
receiptLink = discoverer.findLinkWithRel(RECEIPT_REL, pollResponse.getContentAsString());
} else if (status == HttpStatus.NO_CONTENT.value()) {
action.andExpect(content().string(isEmptyOrNullString()));
}
if (receiptLink == null) {
Thread.sleep(2000);
}
} while (receiptLink == null);
return pollResponse;
}
/**
* Concludes the {@link Order} by looking up the {@code receipt} link from the response and follows it. Triggers a
* {@code DELETE} request susequently.
*
* @param response
* @return
* @throws Exception
*/
private MockHttpServletResponse takeReceipt(MockHttpServletResponse response) throws Exception {
Link receiptLink = getDiscovererFor(response).findLinkWithRel(RECEIPT_REL, response.getContentAsString());
MockHttpServletResponse receiptResponse = mvc.perform(get(receiptLink.getHref())). //
andExpect(status().isOk()). //
andReturn().getResponse();
LOG.info("Accessing receipt, got:" + receiptResponse.getContentAsString());
LOG.info("Taking receipt…");
return mvc
.perform( //
delete(receiptLink.getHref()).//
accept(MediaTypes.HAL_JSON))
. //
andExpect(status().isOk()). //
andReturn().getResponse();
}
/**
* Follows the {@code order} link and asserts only the self link being present so that no further navigation is
* possible anymore.
*
* @param response
* @throws Exception
*/
private void verifyOrderTaken(MockHttpServletResponse response) throws Exception {
Link orderLink = getDiscovererFor(response).findLinkWithRel(ORDER_REL, response.getContentAsString());
MockHttpServletResponse orderResponse = mvc.perform(get(orderLink.expand().getHref())). //
andExpect(status().isOk()). // //
andExpect(linkWithRelIsPresent(Link.REL_SELF)). //
andExpect(linkWithRelIsNotPresent(UPDATE_REL)). //
andExpect(linkWithRelIsNotPresent(CANCEL_REL)). //
andExpect(linkWithRelIsNotPresent(PAYMENT_REL)). //
andExpect(jsonPath("$.status", is("Delivered"))). //
andReturn().getResponse();
LOG.info("Final order state: " + orderResponse.getContentAsString());
}
/**
* Cancels the order by issuing a delete request. Verifies the resource being inavailable after that.
*
* @param response the response that retrieved an order resource
* @throws Exception
*/
private void cancelOrder(MockHttpServletResponse response) throws Exception {
String content = response.getContentAsString();
LinkDiscoverer discoverer = getDiscovererFor(response);
Link selfLink = discoverer.findLinkWithRel(Link.REL_SELF, content).expand();
Link cancellationLink = discoverer.findLinkWithRel(CANCEL_REL, content);
mvc.perform(delete(cancellationLink.getHref())).andExpect(status().isNoContent());
mvc.perform(get(selfLink.getHref())).andExpect(status().isNotFound());
}
}